🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / core / service / src / androidMain / kotlin / org / meshtastic / core / service / ConversationShortcutPublisher.kt
Displaying Raw • Download
core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt fix/qr-error-correction (7b45f84f) Text, 15.84 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.service
Tff7b72import T7ee787android.content.Context
Tff7b72import T7ee787android.content.Intent
Tff7b72import T7ee787android.content.pm.ShortcutInfo
Tff7b72import T7ee787android.graphics.Color
Tff7b72import T7ee787androidx.core.app.Person
Tff7b72import T7ee787androidx.core.content.LocusIdCompat
Tff7b72import T7ee787androidx.core.content.pm.ShortcutInfoCompat
Tff7b72import T7ee787androidx.core.content.pm.ShortcutManagerCompat
Tff7b72import T7ee787androidx.core.graphics.drawable.IconCompat
Tff7b72import T7ee787androidx.core.net.toUri
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.flow.combine
Tff7b72import T7ee787kotlinx.coroutines.flow.distinctUntilChanged
Tff7b72import T7ee787kotlinx.coroutines.flow.map
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787org.koin.core.annotation.Single
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.Channel
Tff7b72import T7ee787org.meshtastic.core.model.DataPacket
Tff7b72import T7ee787org.meshtastic.core.model.NodeAddress
Tff7b72import T7ee787org.meshtastic.core.model.nodeColorsFromNum
Tff7b72import T7ee787org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
Tff7b72import T7ee787org.meshtastic.core.repository.NodeRepository
Tff7b72import T7ee787org.meshtastic.core.repository.PacketRepository
Tff7b72import T7ee787org.meshtastic.core.repository.RadioConfigRepository
Tff7b72import T7ee787org.meshtastic.core.resources.Res
Tff7b72import T7ee787org.meshtastic.core.resources.getString
Tff7b72import T7ee787org.meshtastic.core.resources.getStringSuspend
Tff7b72import T7ee787org.meshtastic.core.resources.unknown_username
Tff7b72import T7ee787org.meshtastic.proto.ChannelSet
Tff7b72import T7ee787java.util.concurrent.ConcurrentHashMap
T8b949e/**
* Single owner of the dynamic conversation shortcuts that link Meshtastic conversations to their notifications.
*
* Publishing lives here in `core:service` (rather than in a feature module) so that shortcuts exist whenever the mesh
* service is running — not only during an Android Auto session. This is what lets [MeshNotificationManagerImpl] attach
* `setShortcutId`/`setLocusId` to message notifications so Android ranks them in the shade's Conversations section and
* surfaces them to Android Auto/Wear.
*
* Two publishing paths, both keyed by `contactKey`:
* - [startObserving] keeps a full set of DM + channel shortcuts in sync with the database for the service lifetime.
* - [ensureConversationShortcut] publishes a single shortcut on demand when a notification is about to reference one
* that the observer has not emitted yet (e.g. the very first message of a brand-new conversation).
*/
Tf0883e@Single
Tff7b72class T56d364ConversationShortcutPublisherTb4b4b4(
Tff7b72private Tff7b72val Te6edf3contextTb4b4b4: Te6edf3ContextTb4b4b4,
Tff7b72private Tff7b72val Te6edf3nodeRepositoryTb4b4b4: Te6edf3NodeRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3packetRepositoryTb4b4b4: Te6edf3PacketRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3radioConfigRepositoryTb4b4b4: Te6edf3RadioConfigRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3dispatchersTb4b4b4: Te6edf3CoroutineDispatchersTb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72private Tff7b72var Te6edf3observeJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null
T8b949e/**
* On-demand shortcut ids published ahead of the observed contacts snapshot (a brand-new conversation's first
* notification). Protected from pruning until a snapshot includes them, so [publishShortcuts] can't sever a live
* notification's shortcut link during the window before the flow catches up.
*/
Tff7b72private Tff7b72val Te6edf3pendingOnDemandIds Tff7b72= Te6edf3ConcurrentHashMapTb4b4b4.Te6edf3newKeySetTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72fun Td2a8ffstartObservingTb4b4b4(Te6edf3scopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4) Tb4b4b4{
Te6edf3observeJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3observeJob Tff7b72=
T8b949e// The caller's scope may be Main-bound (MeshService); shortcut publishing does ShortcutManager IPC and
T8b949e// rasterizes avatar bitmaps on every recency-affecting DB change, so keep it off the Main thread.
Te6edf3scopeTb4b4b4.Te6edf3launchTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
T8b949e// Combine the message DB (recency + DM peers) with the channel config (names). Ranking both by
T8b949e// last-activity time lets launchers / Android Auto surface the *recently active* conversations first
T8b949e// (the shortcut display cap is small), instead of an arbitrary slice of stale channels.
Te6edf3combineTb4b4b4(Te6edf3packetRepositoryTb4b4b4.Te6edf3getContactsTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3channelSetFlowTb4b4b4) Tb4b4b4{ Te6edf3contactsTb4b4b4, Te6edf3channelSet Tff7b72-Tff7b72>
Te6edf3rankConversationsByRecencyTb4b4b4(Te6edf3contactsTb4b4b4, Te6edf3channelSetTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3distinctUntilChangedTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3collect Tb4b4b4{ Te6edf3conversations Tff7b72-Tff7b72> Te6edf3publishShortcutsTb4b4b4(Te6edf3conversationsTb4b4b4) Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffrankConversationsByRecencyTb4b4b4(
Te6edf3contactsTb4b4b4: Te6edf3MapTff7b72<Tffa657StringTb4b4b4, Te6edf3DataPacketTff7b72>Tb4b4b4,
Te6edf3channelSetTb4b4b4: Te6edf3ChannelSetTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3ConversationTff7b72> Tb4b4b4{
Tff7b72val Te6edf3lora Tff7b72= Te6edf3channelSetTb4b4b4.Te6edf3lora_config Tff7b72?: Te6edf3ChannelTb4b4b4.Te6edf3defaultTb4b4b4.Te6edf3loraConfig
Tff7b72val Te6edf3dms Tff7b72=
Te6edf3contactsTb4b4b4.Te6edf3entries
T8b949e// DM contacts are those whose key does NOT contain the broadcast ID.
Tb4b4b4.Te6edf3filter Tb4b4b4{ Tb4b4b4(Te6edf3keyTb4b4b4, Te6edf3_Tb4b4b4) Tff7b72-Tff7b72> Tff7b72!Te6edf3keyTb4b4b4.Te6edf3containsTb4b4b4(Te6edf3NodeAddressTb4b4b4.Te6edf3ID_BROADCASTTb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3map Tb4b4b4{ Tb4b4b4(Te6edf3keyTb4b4b4, Te6edf3packetTb4b4b4) Tff7b72-Tff7b72> Te6edf3ConversationTb4b4b4.Te6edf3DmTb4b4b4(Te6edf3keyTb4b4b4, Te6edf3packetTb4b4b4.Te6edf3fromTb4b4b4.Te6edf3orEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3packetTb4b4b4.Te6edf3timeTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3channels Tff7b72=
Te6edf3channelSetTb4b4b4.Te6edf3settingsTb4b4b4.Te6edf3mapIndexedNotNull Tb4b4b4{ Te6edf3indexTb4b4b4, Te6edf3settings Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3index Tff7b72=Tff7b72= T79c0ff0 Tff7b72|Tff7b72| Te6edf3settingsTb4b4b4.Te6edf3nameTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3contactKey Tff7b72= Ta5d6ff"Tffd700$Te6edf3indexTffd700${Te6edf3NodeAddressTb4b4b4.Te6edf3ID_BROADCASTTffd700}Ta5d6ff"
T8b949e// Effective name: an empty primary shows its modem-preset name ("LongFast", …). Recency comes from
T8b949e// the channel's broadcast contact entry, if any.
Te6edf3ConversationTb4b4b4.Te6edf3ChannelTb4b4b4(
Te6edf3contactKeyTb4b4b4,
Te6edf3indexTb4b4b4,
Te6edf3ChannelTb4b4b4(Te6edf3settingsTb4b4b4, Te6edf3loraTb4b4b4)Tb4b4b4.Te6edf3nameTb4b4b4,
Te6edf3contactsTff7b72[Te6edf3contactKeyTff7b72]Tff7b72?.Te6edf3time Tff7b72?: T79c0ff0LTb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Tff7b72null
Tb4b4b4}
Tb4b4b4}
T8b949e// Most recently active first; conversations with no messages (time 0) sort last.
Tff7b72return Tb4b4b4(Te6edf3dms Tff7b72+ Te6edf3channelsTb4b4b4)Tb4b4b4.Te6edf3sortedByDescending Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3time Tb4b4b4}
Tb4b4b4}
Tff7b72fun Td2a8ffstopObservingTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3observeJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3observeJob Tff7b72= Tff7b72null
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffpublishShortcutsTb4b4b4(Te6edf3conversationsTb4b4b4: Te6edf3ListTff7b72<Te6edf3ConversationTff7b72>Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3limit Tff7b72= Te6edf3ShortcutManagerCompatTb4b4b4.Te6edf3getMaxShortcutCountPerActivityTb4b4b4(Te6edf3contextTb4b4b4)
T8b949e// rank == list position, so the most recent conversation is rank 0 and shown first.
Tff7b72val Te6edf3shortcuts Tff7b72=
Te6edf3conversationsTb4b4b4.Te6edf3takeTb4b4b4(Te6edf3limitTb4b4b4)Tb4b4b4.Te6edf3mapIndexedNotNull Tb4b4b4{ Te6edf3rankTb4b4b4, Te6edf3conversation Tff7b72-Tff7b72>
Tff7b72when Tb4b4b4(Te6edf3conversationTb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3ConversationTb4b4b4.Te6edf3Dm Tff7b72-Tff7b72> Te6edf3buildDmShortcutTb4b4b4(Te6edf3conversationTb4b4b4, Te6edf3rankTb4b4b4)
Tff7b72is Te6edf3ConversationTb4b4b4.Te6edf3Channel Tff7b72-Tff7b72> Te6edf3buildChannelShortcutTb4b4b4(Te6edf3conversationTb4b4b4, Te6edf3rankTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3currentKeys Tff7b72= Te6edf3shortcutsTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)
T8b949e// The snapshot now covers these conversations; normal pruning applies to them from here on.
Te6edf3pendingOnDemandIdsTb4b4b4.Te6edf3removeAllTb4b4b4(Te6edf3currentKeysTb4b4b4)
Tff7b72val Te6edf3stale Tff7b72=
Te6edf3ShortcutManagerCompatTb4b4b4.Te6edf3getDynamicShortcutsTb4b4b4(Te6edf3contextTb4b4b4)
Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tb4b4b4}
Tb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657it Tff7b72!in Te6edf3currentKeys Tff7b72&Tff7b72& Tffa657it Tff7b72!in Te6edf3pendingOnDemandIds Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3staleTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3ShortcutManagerCompatTb4b4b4.Te6edf3removeDynamicShortcutsTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3staleTb4b4b4)
Tb4b4b4}
Tff7b72for Tb4b4b4(Te6edf3shortcut Tff7b72in Te6edf3shortcutsTb4b4b4) Tb4b4b4{
Te6edf3ShortcutManagerCompatTb4b4b4.Te6edf3pushDynamicShortcutTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3shortcutTb4b4b4)
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3dTb4b4b4(Te6edf3tag Tff7b72= Te6edf3TAGTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffPublished Tffd700${Te6edf3shortcutsTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff conversation shortcuts (recency-ranked)Ta5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3IllegalArgumentExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3tag Tff7b72= Te6edf3TAGTb4b4b4, Te6edf3throwable Tff7b72= Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to publish conversation shortcutsTa5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3IllegalStateExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3tag Tff7b72= Te6edf3TAGTb4b4b4, Te6edf3throwable Tff7b72= Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to publish conversation shortcutsTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffbuildDmShortcutTb4b4b4(Te6edf3dmTb4b4b4: Te6edf3ConversationTb4b4b4.Te6edf3DmTb4b4b4, Te6edf3rankTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3ShortcutInfoCompat? Tb4b4b4{
Tff7b72val Te6edf3node Tff7b72= Te6edf3nodeRepositoryTb4b4b4.Te6edf3nodeDBbyNumTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3userTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Te6edf3dmTb4b4b4.Te6edf3userId Tb4b4b4}
T8b949e// shortLabel is the compact 4-char short name; longLabel the full node name. Fall back to a localized generic
T8b949e// name when node metadata is missing: the raw contactKey is a user-traceable identifier and must stay confined
T8b949e// to internal ids and deep-link metadata (privacy-first convention).
Tff7b72val Te6edf3shortName Tff7b72= Te6edf3nodeTff7b72?.Te6edf3userTff7b72?.Te6edf3short_nameTff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isNotBlankTb4b4b4(Tb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3longName Tff7b72= Te6edf3nodeTff7b72?.Te6edf3userTff7b72?.Te6edf3long_nameTff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isNotBlankTb4b4b4(Tb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3fallbackName Tff7b72by Te6edf3lazy Tb4b4b4{ Te6edf3getStringTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3unknown_usernameTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3shortLabel Tff7b72= Te6edf3shortName Tff7b72?: Te6edf3longName Tff7b72?: Te6edf3fallbackName
Tff7b72val Te6edf3longLabel Tff7b72= Te6edf3longName Tff7b72?: Te6edf3shortName Tff7b72?: Te6edf3fallbackName
T8b949e// A node-colored pill avatar showing the short name identifies the person and matches the in-app node chip.
T8b949e// Set it on the shortcut itself (not just the Person) so launchers/Android Auto render it instead of a generic
T8b949e// head silhouette.
Tff7b72val Te6edf3icon Tff7b72=
Te6edf3nodeTff7b72?.Te6edf3let Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3foregroundColorTb4b4b4, Te6edf3backgroundColorTb4b4b4) Tff7b72= Te6edf3nodeColorsFromNumTb4b4b4(Tffa657itTb4b4b4.Te6edf3numTb4b4b4)
Te6edf3PersonIconFactoryTb4b4b4.Te6edf3createLabelTb4b4b4(Te6edf3shortLabelTb4b4b4, Te6edf3backgroundColorTb4b4b4, Te6edf3foregroundColorTb4b4b4, Te6edf3rounded Tff7b72= Tff7b72falseTb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3person Tff7b72=
Te6edf3PersonTb4b4b4.Te6edf3BuilderTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3setNameTb4b4b4(Te6edf3longLabelTb4b4b4)
Tb4b4b4.Te6edf3setKeyTb4b4b4(Te6edf3dmTb4b4b4.Te6edf3contactKeyTb4b4b4)
T8b949e// Favorite nodes feed the system's conversation-priority ranking.
Tb4b4b4.Te6edf3setImportantTb4b4b4(Te6edf3nodeTff7b72?.Te6edf3isFavorite Tff7b72=Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3iconTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3setIconTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3ShortcutInfoCompatTb4b4b4.Te6edf3BuilderTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3dmTb4b4b4.Te6edf3contactKeyTb4b4b4)
Tb4b4b4.Te6edf3setShortLabelTb4b4b4(Te6edf3shortLabelTb4b4b4)
Tb4b4b4.Te6edf3setLongLabelTb4b4b4(Te6edf3longLabelTb4b4b4)
Tb4b4b4.Te6edf3setRankTb4b4b4(Te6edf3rankTb4b4b4)
Tb4b4b4.Te6edf3setLocusIdTb4b4b4(Te6edf3LocusIdCompatTb4b4b4(Te6edf3dmTb4b4b4.Te6edf3contactKeyTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3setPersonTb4b4b4(Te6edf3personTb4b4b4)
Tb4b4b4.Te6edf3setLongLivedTb4b4b4(Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3setCategoriesTb4b4b4(Te6edf3setOfTb4b4b4(Te6edf3ShortcutInfoTb4b4b4.Te6edf3SHORTCUT_CATEGORY_CONVERSATIONTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3setIntentTb4b4b4(Te6edf3conversationIntentTb4b4b4(Te6edf3dmTb4b4b4.Te6edf3contactKeyTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3iconTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3setIconTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffbuildChannelShortcutTb4b4b4(Te6edf3channelTb4b4b4: Te6edf3ConversationTb4b4b4.Te6edf3ChannelTb4b4b4, Te6edf3rankTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3ShortcutInfoCompat Tb4b4b4{
T8b949e// channel.name is the effective name and never empty: Channel.name resolves an empty settings.name to the
T8b949e// modem-preset display name (or "Custom" for non-preset configs), so no fallback placeholder is needed.
Tff7b72val Te6edf3channelName Tff7b72= Te6edf3channelTb4b4b4.Te6edf3name
T8b949e// A black rounded-square badge with the channel number visually separates channels from the node-colored DM
T8b949e// pills.
Tff7b72val Te6edf3icon Tff7b72= Te6edf3channelIconTb4b4b4(Te6edf3channelTb4b4b4.Te6edf3indexTb4b4b4)
Tff7b72val Te6edf3person Tff7b72= Te6edf3PersonTb4b4b4.Te6edf3BuilderTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3setNameTb4b4b4(Te6edf3channelNameTb4b4b4)Tb4b4b4.Te6edf3setKeyTb4b4b4(Ta5d6ff"Ta5d6ffchannel-Tffd700${Te6edf3channelTb4b4b4.Te6edf3indexTffd700}Ta5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3setIconTb4b4b4(Te6edf3iconTb4b4b4)Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3ShortcutInfoCompatTb4b4b4.Te6edf3BuilderTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3channelTb4b4b4.Te6edf3contactKeyTb4b4b4)
Tb4b4b4.Te6edf3setShortLabelTb4b4b4(Te6edf3channelNameTb4b4b4)
Tb4b4b4.Te6edf3setLongLabelTb4b4b4(Te6edf3channelNameTb4b4b4)
Tb4b4b4.Te6edf3setRankTb4b4b4(Te6edf3rankTb4b4b4)
Tb4b4b4.Te6edf3setLocusIdTb4b4b4(Te6edf3LocusIdCompatTb4b4b4(Te6edf3channelTb4b4b4.Te6edf3contactKeyTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3setPersonTb4b4b4(Te6edf3personTb4b4b4)
Tb4b4b4.Te6edf3setLongLivedTb4b4b4(Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3setCategoriesTb4b4b4(Te6edf3setOfTb4b4b4(Te6edf3ShortcutInfoTb4b4b4.Te6edf3SHORTCUT_CATEGORY_CONVERSATIONTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3setIntentTb4b4b4(Te6edf3conversationIntentTb4b4b4(Te6edf3channelTb4b4b4.Te6edf3contactKeyTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3setIconTb4b4b4(Te6edf3iconTb4b4b4)
Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e/** Black chip with a white channel [index] — a consistent, uncolored channel marker. */
Tff7b72private Tff7b72fun Td2a8ffchannelIconTb4b4b4(Te6edf3indexTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3IconCompat Tff7b72= Te6edf3PersonIconFactoryTb4b4b4.Te6edf3createLabelTb4b4b4(
Te6edf3label Tff7b72= Te6edf3indexTb4b4b4.Te6edf3toStringTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3backgroundColor Tff7b72= Te6edf3ColorTb4b4b4.Te6edf3BLACKTb4b4b4,
Te6edf3foregroundColor Tff7b72= Te6edf3ColorTb4b4b4.Te6edf3WHITETb4b4b4,
Te6edf3rounded Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffconversationIntentTb4b4b4(Te6edf3contactKeyTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3Intent Tff7b72=
Te6edf3IntentTb4b4b4(Te6edf3IntentTb4b4b4.Te6edf3ACTION_VIEWTb4b4b4, Ta5d6ff"Tffd700$Te6edf3DEEP_LINK_BASE_URITa5d6ff/messages/Tffd700$Te6edf3contactKeyTa5d6ff"Tb4b4b4.Te6edf3toUriTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3setPackageTb4b4b4(Te6edf3contextTb4b4b4.Te6edf3packageNameTb4b4b4)
Tb4b4b4}
T8b949e/**
* Ensures a long-lived conversation shortcut exists for [contactKey] before a notification references it. A no-op
* when [startObserving] has already published the shortcut; otherwise publishes a minimal one so the notification's
* `setShortcutId`/`setLocusId` resolve immediately (the observer replaces it with a richer version on its next
* emission).
*/
Tff7b72suspend Tff7b72fun Td2a8ffensureConversationShortcutTb4b4b4(Te6edf3contactKeyTb4b4b4: Tffa657StringTb4b4b4, Te6edf3displayNameTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
T8b949e// Protect this conversation from the observer's pruning until a contacts snapshot includes it.
Te6edf3pendingOnDemandIds Tff7b72+Tff7b72= Te6edf3contactKey
Tff7b72val Te6edf3alreadyPublished Tff7b72= Te6edf3ShortcutManagerCompatTb4b4b4.Te6edf3getDynamicShortcutsTb4b4b4(Te6edf3contextTb4b4b4)Tb4b4b4.Te6edf3any Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Te6edf3contactKey Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3alreadyPublishedTb4b4b4) Tff7b72return
T8b949e// ShortcutInfoCompat.Builder.build() rejects a blank short label. On-demand callers derive the name from packet
T8b949e// metadata that can be empty (an unnamed node, or a reaction that arrives before the NodeDB knows the sender),
T8b949e// so fall back to the same localized placeholder the observer uses rather than letting build() throw.
Tff7b72val Te6edf3label Tff7b72= Te6edf3displayNameTb4b4b4.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isNotBlankTb4b4b4(Tb4b4b4) Tb4b4b4} Tff7b72?: Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3unknown_usernameTb4b4b4)
T8b949e// Match the styling the observer will republish so there is no generic-head flash: rounded channel badge with
T8b949e// its number, or a circular initial for a DM. Color is derived from the key (the node object may not be known
T8b949e// yet at on-demand time).
Tff7b72val Te6edf3isChannel Tff7b72= Te6edf3contactKeyTb4b4b4.Te6edf3containsTb4b4b4(Te6edf3NodeAddressTb4b4b4.Te6edf3ID_BROADCASTTb4b4b4)
Tff7b72val Te6edf3icon Tff7b72=
Tff7b72if Tb4b4b4(Te6edf3isChannelTb4b4b4) Tb4b4b4{
Te6edf3channelIconTb4b4b4(Te6edf3contactKeyTb4b4b4.Te6edf3substringBeforeTb4b4b4(Te6edf3NodeAddressTb4b4b4.Te6edf3ID_BROADCASTTb4b4b4)Tb4b4b4.Te6edf3toIntOrNullTb4b4b4(Tb4b4b4) Tff7b72?: T79c0ff0Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3foregroundColorTb4b4b4, Te6edf3backgroundColorTb4b4b4) Tff7b72= Te6edf3nodeColorsFromNumTb4b4b4(Te6edf3contactKeyTb4b4b4.Te6edf3hashCodeTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3PersonIconFactoryTb4b4b4.Te6edf3createTb4b4b4(Te6edf3labelTb4b4b4, Te6edf3backgroundColorTb4b4b4, Te6edf3foregroundColorTb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3person Tff7b72= Te6edf3PersonTb4b4b4.Te6edf3BuilderTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3setNameTb4b4b4(Te6edf3labelTb4b4b4)Tb4b4b4.Te6edf3setKeyTb4b4b4(Te6edf3contactKeyTb4b4b4)Tb4b4b4.Te6edf3setIconTb4b4b4(Te6edf3iconTb4b4b4)Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)
T8b949e// build() as well as pushDynamicShortcut() enforces the builder's invariants, so both stay inside the guard: an
T8b949e// escaping throw here cancels the caller's coroutine and drops the notification it was about to post.
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3shortcut Tff7b72=
Te6edf3ShortcutInfoCompatTb4b4b4.Te6edf3BuilderTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3contactKeyTb4b4b4)
Tb4b4b4.Te6edf3setShortLabelTb4b4b4(Te6edf3labelTb4b4b4)
Tb4b4b4.Te6edf3setLongLabelTb4b4b4(Te6edf3labelTb4b4b4)
Tb4b4b4.Te6edf3setLocusIdTb4b4b4(Te6edf3LocusIdCompatTb4b4b4(Te6edf3contactKeyTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3setPersonTb4b4b4(Te6edf3personTb4b4b4)
Tb4b4b4.Te6edf3setLongLivedTb4b4b4(Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3setCategoriesTb4b4b4(Te6edf3setOfTb4b4b4(Te6edf3ShortcutInfoTb4b4b4.Te6edf3SHORTCUT_CATEGORY_CONVERSATIONTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3setIntentTb4b4b4(Te6edf3conversationIntentTb4b4b4(Te6edf3contactKeyTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3setIconTb4b4b4(Te6edf3iconTb4b4b4)
Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)
Te6edf3ShortcutManagerCompatTb4b4b4.Te6edf3pushDynamicShortcutTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3shortcutTb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3IllegalArgumentExceptionTb4b4b4) Tb4b4b4{
T8b949e// Don't log the contactKey: node/channel identifiers are user-traceable (privacy-first convention).
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3tag Tff7b72= Te6edf3TAGTb4b4b4, Te6edf3throwable Tff7b72= Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to publish on-demand shortcutTa5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3IllegalStateExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3tag Tff7b72= Te6edf3TAGTb4b4b4, Te6edf3throwable Tff7b72= Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to publish on-demand shortcutTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/** A publishable conversation with its last-activity [time], used to rank shortcuts by recency. */
Tff7b72private Tff7b72sealed Tff7b72interface T56d364Conversation Tb4b4b4{
Tff7b72val Te6edf3contactKeyTb4b4b4: Tffa657String
Tff7b72val Te6edf3timeTb4b4b4: Tffa657Long
Tff7b72data Tff7b72class T56d364DmTb4b4b4(Tff7b72override Tff7b72val Te6edf3contactKeyTb4b4b4: Tffa657StringTb4b4b4, Tff7b72val Te6edf3userIdTb4b4b4: Tffa657StringTb4b4b4, Tff7b72override Tff7b72val Te6edf3timeTb4b4b4: Tffa657LongTb4b4b4) Tb4b4b4: Te6edf3Conversation
Tff7b72data Tff7b72class T56d364ChannelTb4b4b4(Tff7b72override Tff7b72val Te6edf3contactKeyTb4b4b4: Tffa657StringTb4b4b4, Tff7b72val Te6edf3indexTb4b4b4: Tffa657IntTb4b4b4, Tff7b72val Te6edf3nameTb4b4b4: Tffa657StringTb4b4b4, Tff7b72override Tff7b72val Te6edf3timeTb4b4b4: Tffa657LongTb4b4b4) Tb4b4b4:
Te6edf3Conversation
Tb4b4b4}
Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72private Tff7b72const Tff7b72val Te6edf3TAG Tff7b72= Ta5d6ff"Ta5d6ffConversationShortcutsTa5d6ff"
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.09s